import 'dart:convert';
import 'dart:typed_data';

import 'package:dbio_utils/bam/util.dart';
import 'package:dbio_utils/generic_filehandle/generic_file_handle.dart';
import 'package:dbio_utils/generic_filehandle/remote_file.dart';
import 'package:dbio_utils/tabix/index_file.dart';
import 'package:dbio_utils/tabix/virtual_offset.dart';
import 'package:flutter/services.dart';

import 'bam_file.dart';

import '../tabix/chunk.dart';
import 'package:dbio_utils/bgzf_filehandle/unzip.dart';

import 'sam.dart';

class HeaderLine {
  String tag;
  String value;
}

class HtsgetSource {
  String url;
  Map<String, String> headers;
}

Future<List<int>> concat(List<HtsgetSource> arr, Map<String, dynamic> opts) async {
  Future<List<int>> itemMap(HtsgetSource chunk) async {
    if (chunk.url.startsWith('data:')) {
      return base64.decode(chunk.url.split(',')[1]);
    }
    var referer = chunk.headers['referer'];
    List<int> res = await RemoteFile(url: chunk.url).fetch(chunk.url, {...chunk.headers, ...(opts['headers'] ?? {})});
    if (null == res) {
      throw Exception('Failed to fetch ${chunk.url}');
    }
    return res;
  }

  List<List<int>> bytesList = await Future.wait(arr.map(itemMap).toList());
  bytesList = bytesList.map((bytes) => unzip(bytes));
  List<int> _bytes = [];
  for (List<int> bytes in bytesList) {
    _bytes.addAll(bytes);
  }
  return _bytes;
}

class HtsgetFile extends BamFile {
  String baseUrl;
  String trackId;

  HtsgetFile({
    GenericFileHandle bamFile,
    IndexFile baiFile,
    this.baseUrl,
    this.trackId,
  }) : super(bam: bamFile, index: baiFile);

  streamRecordsForRange(
    String chr,
    int min,
    int max, [
    Map opts = const {'viewAsPairs': false, 'pairAcrossChr': false, 'maxInsertSize': 200000},
  ]) async {
    var base = '${this.baseUrl}/${this.trackId}';
    var url = '${base}?referenceName=${chr}&start=${min}&end=${max}&format=BAM';
    var chrId = this.chrToIndex[chr];
    var result = await RemoteFile(url: url).readFile();
    if (result == null) {
      throw new Exception('Failed to fetch ${url}');
    }
    var data = json.decode(utf8.decode(result));
    var uncba = await concat(data.htsget.urls.slice(1), opts);

    HtsChunk chunk = HtsChunk(chr, VirtualOffset(0, 0), uncba, min, max);
    // var chunk = {
    // 'buffer': uncba,
    // 'chunk': {
    //   'minv': {'dataPosition': 0}
    // },
    // toString() {
    // return `${chr}_${min}_${max}`
    // },
    // };
    this.fetchChunkFeatures([chunk], chrId, min, max, opts);
  }

  //@ts-ignore
  Future<ChunkSlice> readChunk(Chunk chunk, Map opts) async {
    HtsChunk _chunk = chunk;
    return ChunkSlice({
      'data': _chunk.buffer,
      'cpositions': [0],
      'dpositions': [0],
      chunk: chunk
    });
  }

  getHeader(Map opts) async {
    var url = '${this.baseUrl}/${this.trackId}?referenceName=na&class=header';
    var result = await RemoteFile(url: url).readFile();
    if (result == null) {
      throw new Exception('Failed to fetch ${url}');
    }
    var data = json.decode(utf8.decode(result));
    var uncba = await concat(data['htsget']['urls'], opts);

    ByteData byteData = ByteData.view(Uint8List.fromList(uncba).buffer);

    if (byteData.getInt32(0, Endian.little) != BAM_MAGIC) {
      throw new Exception('Not a BAM file');
    }
    var headLen = byteData.getInt32(4, Endian.little);

    var headerText = utf8.decode(uncba.sublist(8, 8 + headLen)); // uncba.toString('utf8', 8, 8 + headLen);
    var samHeader = parseHeaderText(headerText);

    // use the @SQ lines in the header to figure out the
    // mapping between ref ref ID numbers and names
    List<String> idToName = [];
    Map<String, int> nameToId = {};
    var sqLines = samHeader.where((l) => l['tag'] == 'SQ');
    sqLines.forEach((e) {
      Map sqLine = e['sqLine'];
      int refId = e['refId'];
      sqLine['data'].forEach((HeaderLine item) {
        if (item.tag == 'SN') {
          // this is the ref name
          var refName = item.value;
          nameToId[refName] = refId;
          idToName[refId] = refName;
        }
      });
    });
    this.chrToIndex = nameToId;
    this.indexToChr = idToName;
    return samHeader;
  }
}
